﻿// --------------------------------------------------------------------------------------------------------------------
// <copyright file="InvokeMethodBinding.cs" company="JDML Consulting Ltd.">
//   Copyright 2011 Photon Project - http://photon.codeplex.com
// </copyright>
// --------------------------------------------------------------------------------------------------------------------

namespace Photon.Windows.Input
{
    using System;
    using System.Collections.Generic;
    using System.Collections.ObjectModel;
    using System.ComponentModel.DataAnnotations;
    using System.Linq;
    using System.Reflection;
    using System.Text;

    public class InvokeMethodBinding
    {
        public InvokeMethodBinding(MethodInfo executeMethod, MethodInfo canExecuteMethod)
        {
            ExecuteMethod = executeMethod;
            CanExecuteMethod = canExecuteMethod;
            ExecuteMethod.GetParameters();
        }

        public string DisplayName
        {
            get
            {
                var attribute = GetAttribute<DisplayAttribute>();
                if (attribute != null && !string.IsNullOrEmpty(attribute.Name))
                {
                    return attribute.Name;
                }

                return FormatDisplayName();
            }
        }

        public MethodInfo CanExecuteMethod { get; private set; }

        public MethodInfo ExecuteMethod { get; private set; }

        public bool CanExecute(object target, IList<object> arguments)
        {
            object[] boundArguments;
            return TryPrepareExecute(target, arguments, out boundArguments);
        }

        public void Execute(object target, IList<object> arguments)
        {
            object[] boundArguments;
            if (TryPrepareExecute(target, arguments, out boundArguments))
            {
                ExecuteMethod.Invoke(target, boundArguments);
            }
        }

        private bool TryPrepareExecute(object target, IList<object> arguments, out object[] boundArguments)
        {
            if (!TryBindArguments(ExecuteMethod, arguments, out boundArguments))
            {
                return false;
            }

            if (EvaluateCanExecute(target, boundArguments))
            {
                return true;
            }

            boundArguments = null;
            return false;
        }

        private bool EvaluateCanExecute(object target, object[] arguments)
        {
            if (CanExecuteMethod == null)
            {
                return true;
            }

            var parameters = CanExecuteMethod.GetParameters();
            if (parameters.Count() != arguments.Length)
            {
                arguments = arguments.Take(parameters.Length).ToArray();
            }

            return (bool)CanExecuteMethod.Invoke(target, arguments);
        }

        private bool TryBindArguments(MethodInfo method, IList<object> arguments, out object[] boundArguments)
        {
            boundArguments = null;
            var parameters = method.GetParameters();
            if (parameters.Length > arguments.Count)
            {
                return false;
            }

            boundArguments = ConvertArguments(parameters, arguments);
            if (boundArguments == null || !ValidateArguments(parameters, boundArguments))
            {
                boundArguments = null;
                return false;
            }

            return true;
        }

        private static object[] ConvertArguments(IList<ParameterInfo> parameters, IList<object> arguments)
        {
            var result = new object[parameters.Count];
            return parameters.Where((parameter, i) => !DefaultValueConverter.Instance.TryChangeType(
                arguments[i], parameter.ParameterType, out result[i])).Any()
                ? null
                : result;
        }

        private bool ValidateArguments(IList<ParameterInfo> parameters, IList<object> arguments)
        {
            if (parameters.Count == 0)
            {
                return true;
            }

            var validationContext = new ValidationContext(this, null, new Dictionary<object, object>());

            Collection<ValidationResult> validationResults = null;
            for (var i = 0; i < parameters.Count; i++)
            {
                var attributes =
                    parameters[i].GetCustomAttributes(typeof(ValidationAttribute), true).Cast<ValidationAttribute>();
                if (attributes.Any())
                {
                    if (!Validator.TryValidateValue(arguments[i], validationContext, 
                        validationResults ??
                            (validationResults = new Collection<ValidationResult>()), attributes))
                    {
                        return false;
                    }
                }
            }

            return true;
        }

        private string FormatDisplayName()
        {
            if (ExecuteMethod == null)
            {
                return string.Empty;
            }

            var output = new StringBuilder();
            foreach (var c in ExecuteMethod.Name)
            {
                if (output.Length > 0)
                {
                    if (Char.IsUpper(c) && Char.IsLower(output[output.Length - 1]))
                    {
                        output.Append(' ');
                    }
                }

                output.Append(c);
            }

            return output.ToString();
        }

        public TAttribute GetAttribute<TAttribute>() where TAttribute : Attribute
        {
            return (TAttribute)Attribute.GetCustomAttribute(ExecuteMethod, typeof(TAttribute));
        }
    }
}